<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</title>

<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
<script src="../../../scripts/util.js"></script>

<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-file-list.html">

<script>void(0);</script>

<dom-module id="comment-api-mock">
  <template>
    <gr-file-list id="fileList"
        change-comments="[[_changeComments]]"
        on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
    <gr-comment-api id="commentAPI"></gr-comment-api>
  </template>
  <script src="../../diff/gr-comment-api/gr-comment-api-mock.js"></script>
</dom-module>

<test-fixture id="basic">
  <template>
    <comment-api-mock></comment-api-mock>
  </template>
</test-fixture>

<script>
  suite('gr-file-list tests', () => {
    let element;
    let commentApiWrapper;
    let sandbox;
    let saveStub;
    let loadCommentSpy;

    setup(done => {
      sandbox = sinon.sandbox.create();
      stub('gr-rest-api-interface', {
        getLoggedIn() { return Promise.resolve(true); },
        getPreferences() { return Promise.resolve({}); },
        fetchJSON() { return Promise.resolve({}); },
        getDiffComments() { return Promise.resolve({}); },
        getDiffRobotComments() { return Promise.resolve({}); },
        getDiffDrafts() { return Promise.resolve({}); },
      });
      stub('gr-date-formatter', {
        _loadTimeFormat() { return Promise.resolve(''); },
      });
      stub('gr-diff', {
        reload() { return Promise.resolve(); },
      });

      // Element must be wrapped in an element with direct access to the
      // comment API.
      commentApiWrapper = fixture('basic');
      element = commentApiWrapper.$.fileList;
      loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');

      // Stub methods on the changeComments object after changeComments has
      // been initalized.
      commentApiWrapper.loadComments().then(() => {
        sandbox.stub(element.changeComments, 'getPaths').returns({});
        sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
            .returns({meta: {}, left: [], right: []});
        done();
      });

      element.numFilesShown = 200;
      saveStub = sandbox.stub(element, '_saveReviewedState',
          () => { return Promise.resolve(); });
    });

    teardown(() => {
      sandbox.restore();
    });

    test('correct number of files are shown', () => {
      element._files = _.times(500, i => {
        return {__path: '/file' + i, lines_inserted: 9};
      });
      flushAsynchronousOperations();
      assert.equal(
          Polymer.dom(element.root).querySelectorAll('.file-row').length,
          element.numFilesShown);
    });

    test('get file list', done => {
      const getChangeFilesStub = sandbox.stub(element.$.restAPI, 'getChangeFiles',
          () => {
            return Promise.resolve({
              '/COMMIT_MSG': {lines_inserted: 9},
              'tags.html': {lines_deleted: 123},
              'about.txt': {},
            });
          });

      element._getFiles().then(files => {
        const filenames = files.map(f => { return f.__path; });
        assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']);
        assert.deepEqual(files[0], {
          lines_inserted: 9,
          lines_deleted: 0,
          __path: '/COMMIT_MSG',
        });
        assert.deepEqual(files[1], {
          lines_inserted: 0,
          lines_deleted: 0,
          __path: 'about.txt',
        });
        assert.deepEqual(files[2], {
          lines_inserted: 0,
          lines_deleted: 123,
          __path: 'tags.html',
        });

        getChangeFilesStub.restore();
        done();
      });
    });

    test('get file list with change edit', done => {
      element.editLoaded = true;

      sandbox.stub(element.$.restAPI,
          'getChangeEditFiles', () => {
            return Promise.resolve({
              commit: {},
              files: {
                '/COMMIT_MSG': {
                  lines_inserted: 9,
                },
                'tags.html': {
                  lines_deleted: 123,
                },
                'about.txt': {},
              },
            });
          });

      element._getFiles().then(files => {
        const filenames = files.map(f => { return f.__path; });
        assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']);
        assert.deepEqual(files[0], {
          lines_inserted: 9,
          lines_deleted: 0,
          __path: '/COMMIT_MSG',
        });
        assert.deepEqual(files[1], {
          lines_inserted: 0,
          lines_deleted: 0,
          __path: 'about.txt',
        });
        assert.deepEqual(files[2], {
          lines_inserted: 0,
          lines_deleted: 123,
          __path: 'tags.html',
        });

        done();
      });
    });

    test('calculate totals for patch number', () => {
      element._files = [
        {__path: '/COMMIT_MSG', lines_inserted: 9},
        {
          __path: 'file_added_in_rev2.txt',
          lines_inserted: 1,
          lines_deleted: 1,
          size_delta: 10,
          size: 100,
        },
        {
          __path: 'myfile.txt',
          lines_inserted: 1,
          lines_deleted: 1,
          size_delta: 10,
          size: 100,
        },
      ];
      assert.deepEqual(element._patchChange, {
        inserted: 2,
        deleted: 2,
        size_delta_inserted: 0,
        size_delta_deleted: 0,
        total_size: 0,
      });
      assert.isTrue(element._hideBinaryChangeTotals);
      assert.isFalse(element._hideChangeTotals);

      // Test with a commit message that isn't the first file.
      element._files = [
        {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
        {__path: '/COMMIT_MSG', lines_inserted: 9},
        {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
      ];
      assert.deepEqual(element._patchChange, {
        inserted: 2,
        deleted: 2,
        size_delta_inserted: 0,
        size_delta_deleted: 0,
        total_size: 0,
      });
      assert.isTrue(element._hideBinaryChangeTotals);
      assert.isFalse(element._hideChangeTotals);

      // Test with no commit message.
      element._files = [
        {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
        {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
      ];
      assert.deepEqual(element._patchChange, {
        inserted: 2,
        deleted: 2,
        size_delta_inserted: 0,
        size_delta_deleted: 0,
        total_size: 0,
      });
      assert.isTrue(element._hideBinaryChangeTotals);
      assert.isFalse(element._hideChangeTotals);

      // Test with files missing either lines_inserted or lines_deleted.
      element._files = [
        {__path: 'file_added_in_rev2.txt', lines_inserted: 1},
        {__path: 'myfile.txt', lines_deleted: 1},
      ];
      assert.deepEqual(element._patchChange, {
        inserted: 1,
        deleted: 1,
        size_delta_inserted: 0,
        size_delta_deleted: 0,
        total_size: 0,
      });
      assert.isTrue(element._hideBinaryChangeTotals);
      assert.isFalse(element._hideChangeTotals);
    });

    test('binary only files', () => {
      element._files = [
        {__path: '/COMMIT_MSG', lines_inserted: 9},
        {__path: 'file_binary', binary: true, size_delta: 10, size: 100},
        {__path: 'file_binary', binary: true, size_delta: -5, size: 120},
      ];
      assert.deepEqual(element._patchChange, {
        inserted: 0,
        deleted: 0,
        size_delta_inserted: 10,
        size_delta_deleted: -5,
        total_size: 220,
      });
      assert.isFalse(element._hideBinaryChangeTotals);
      assert.isTrue(element._hideChangeTotals);
    });

    test('binary and regular files', () => {
      element._files = [
        {__path: '/COMMIT_MSG', lines_inserted: 9},
        {__path: 'file_binary', binary: true, size_delta: 10, size: 100},
        {__path: 'file_binary', binary: true, size_delta: -5, size: 120},
        {__path: 'myfile.txt', lines_deleted: 5, size_delta: -10, size: 100},
        {__path: 'myfile2.txt', lines_inserted: 10},
      ];
      assert.deepEqual(element._patchChange, {
        inserted: 10,
        deleted: 5,
        size_delta_inserted: 10,
        size_delta_deleted: -5,
        total_size: 220,
      });
      assert.isFalse(element._hideBinaryChangeTotals);
      assert.isFalse(element._hideChangeTotals);
    });

    test('_formatBytes function', () => {
      const table = {
        '64': '+64 B',
        '1023': '+1023 B',
        '1024': '+1 KiB',
        '4096': '+4 KiB',
        '1073741824': '+1 GiB',
        '-64': '-64 B',
        '-1023': '-1023 B',
        '-1024': '-1 KiB',
        '-4096': '-4 KiB',
        '-1073741824': '-1 GiB',
        '0': '+/-0 B',
      };

      for (const bytes in table) {
        if (table.hasOwnProperty(bytes)) {
          assert.equal(element._formatBytes(bytes), table[bytes]);
        }
      }
    });

    test('_formatPercentage function', () => {
      const table = [
        {size: 100,
          delta: 100,
          display: '',
        },
        {size: 195060,
          delta: 64,
          display: '(+0%)',
        },
        {size: 195060,
          delta: -64,
          display: '(-0%)',
        },
        {size: 394892,
          delta: -7128,
          display: '(-2%)',
        },
        {size: 90,
          delta: -10,
          display: '(-10%)',
        },
        {size: 110,
          delta: 10,
          display: '(+10%)',
        },
      ];

      for (const item of table) {
        assert.equal(element._formatPercentage(
            item.size, item.delta), item.display);
      }
    });

    suite('keyboard shortcuts', () => {
      setup(() => {
        element._files = [
          {__path: '/COMMIT_MSG'},
          {__path: 'file_added_in_rev2.txt'},
          {__path: 'myfile.txt'},
        ];
        element.changeNum = '42';
        element.patchRange = {
          basePatchNum: 'PARENT',
          patchNum: '2',
        };
        element.change = {_number: 42};
        element.$.fileCursor.setCursorAtIndex(0);
      });

      test('toggle left diff via shortcut', () => {
        const toggleLeftDiffStub = sandbox.stub();
        // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
        // https://github.com/sinonjs/sinon/issues/781
        const diffsStub = sinon.stub(element, 'diffs', {
          get() {
            return [{toggleLeftDiff: toggleLeftDiffStub}];
          },
        });
        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
        assert.isTrue(toggleLeftDiffStub.calledOnce);
        diffsStub.restore();
      });

      test('keyboard shortcuts', () => {
        flushAsynchronousOperations();

        const items = Polymer.dom(element.root).querySelectorAll('.file-row');
        element.$.fileCursor.stops = items;
        element.$.fileCursor.setCursorAtIndex(0);
        assert.equal(items.length, 3);
        assert.isTrue(items[0].classList.contains('selected'));
        assert.isFalse(items[1].classList.contains('selected'));
        assert.isFalse(items[2].classList.contains('selected'));
        // j with a modifier should not move the cursor.
        MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
        assert.equal(element.$.fileCursor.index, 0);
        MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
        assert.equal(element.$.fileCursor.index, 1);
        assert.equal(element.selectedIndex, 1);
        MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');

        const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
        assert.equal(element.$.fileCursor.index, 2);
        assert.equal(element.selectedIndex, 2);

        // k with a modifier should not move the cursor.
        MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
        assert.equal(element.$.fileCursor.index, 2);

        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
        assert.equal(element.$.fileCursor.index, 1);
        assert.equal(element.selectedIndex, 1);
        MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');

        assert(navStub.lastCall.calledWith(element.change,
            'file_added_in_rev2.txt', '2'),
            'Should navigate to /c/42/2/file_added_in_rev2.txt');

        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
        assert.equal(element.$.fileCursor.index, 0);
        assert.equal(element.selectedIndex, 0);
      });

      test('i key shows/hides selected inline diff', () => {
        sandbox.stub(element, '_expandedPathsChanged');
        flushAsynchronousOperations();
        const files = Polymer.dom(element.root).querySelectorAll('.file-row');
        element.$.fileCursor.stops = files;
        element.$.fileCursor.setCursorAtIndex(0);
        MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
        flushAsynchronousOperations();
        assert.include(element._expandedFilePaths, element.diffs[0].path);
        MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
        flushAsynchronousOperations();
        assert.notInclude(element._expandedFilePaths, element.diffs[0].path);
        element.$.fileCursor.setCursorAtIndex(1);
        MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
        flushAsynchronousOperations();
        assert.include(element._expandedFilePaths, element.diffs[1].path);

        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
        flushAsynchronousOperations();
        for (const index in element.diffs) {
          if (!element.diffs.hasOwnProperty(index)) { continue; }
          assert.include(element._expandedFilePaths, element.diffs[index].path);
        }
        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
        flushAsynchronousOperations();
        for (const index in element.diffs) {
          if (!element.diffs.hasOwnProperty(index)) { continue; }
          assert.notInclude(element._expandedFilePaths,
              element.diffs[index].path);
        }
      });

      test('r key toggles reviewed flag', () => {
        flushAsynchronousOperations();

        // Default state should be unreviewed.
        assert.equal(element._reviewed.length, 0);

        // Press the review key to toggle it (set the flag).
        MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
        assert.equal(element._reviewed.length, 1);

        // Press the review key to toggle it (clear the flag).
        MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
        assert.equal(element._reviewed.length, 0);
      });

      suite('_handleOKey', () => {
        let interact;

        setup(() => {
          sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
              .returns(false);
          sandbox.stub(element, 'modifierPressed').returns(false);
          const openCursorStub = sandbox.stub(element, '_openCursorFile');
          const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
          const expandStub = sandbox.stub(element, '_togglePathExpanded');

          interact = function(opt_payload) {
            openCursorStub.reset();
            openSelectedStub.reset();
            expandStub.reset();

            const e = new CustomEvent('fake-keyboard-event', opt_payload);
            sinon.stub(e, 'preventDefault');
            element._handleOKey(e);
            assert.isTrue(e.preventDefault.called);
            const result = {};
            if (openCursorStub.called) {
              result.opened_cursor = true;
            }
            if (openSelectedStub.called) {
              result.opened_selected = true;
            }
            if (expandStub.called) {
              result.expanded = true;
            }
            return result;
          };
        });

        test('open from selected file', () => {
          element._showInlineDiffs = false;
          assert.deepEqual(interact(), {opened_selected: true});
        });

        test('open from diff cursor', () => {
          element._showInlineDiffs = true;
          assert.deepEqual(interact(), {opened_cursor: true});

          // "Show diffs" mode overrides userPrefs.expand_inline_diffs
          element._userPrefs = {expand_inline_diffs: true};
          assert.deepEqual(interact(), {opened_cursor: true});
        });

        test('expand when user prefers', () => {
          element._showInlineDiffs = false;
          assert.deepEqual(interact(), {opened_selected: true});
          element._userPrefs = {};
          assert.deepEqual(interact(), {opened_selected: true});
          element._userPrefs.expand_inline_diffs = true;
          assert.deepEqual(interact(), {expanded: true});
        });
      });
    });

    test('comment filtering', () => {
      const comments = {
        '/COMMIT_MSG': [
          {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
          {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
          {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
        ],
        'myfile.txt': [
          {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
          {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
          {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
        ],
        'unresolved.file': [
          {
            patch_set: 2,
            message: 'wat!?',
            updated: '2017-02-09 16:40:49',
            id: '1',
            unresolved: true,
          },
          {
            patch_set: 2,
            message: 'hi',
            updated: '2017-02-10 16:40:49',
            id: '2',
            in_reply_to: '1',
            unresolved: false,
          },
          {
            patch_set: 2,
            message: 'good news!',
            updated: '2017-02-08 16:40:49',
            id: '3',
            unresolved: true,
          },
        ],
      };
      const drafts = {
        'unresolved.file': [
          {
            patch_set: 2,
            message: 'hi',
            updated: '2017-02-11 16:40:49',
            id: '4',
            in_reply_to: '3',
            unresolved: false,
          },
        ],
      };
      assert.equal(
          element._computeCountString(comments, '1', '/COMMIT_MSG', 'comment'),
          '2 comments');
      assert.equal(
          element._computeCommentsStringMobile(comments, '1', '/COMMIT_MSG'),
          '2c');
      assert.equal(
          element._computeDraftsStringMobile(comments, '1', '/COMMIT_MSG'),
          '2d');
      assert.equal(
          element._computeCountString(comments, '1', 'myfile.txt', 'comment'),
          '1 comment');
      assert.equal(
          element._computeCommentsStringMobile(comments, '1', 'myfile.txt'),
          '1c');
      assert.equal(
          element._computeDraftsStringMobile(comments, '1', 'myfile.txt'),
          '1d');
      assert.equal(
          element._computeCountString(comments, '1',
              'file_added_in_rev2.txt', 'comment'), '');
      assert.equal(
          element._computeCommentsStringMobile(comments, '1',
              'file_added_in_rev2.txt'), '');
      assert.equal(
          element._computeDraftsStringMobile(comments, '1',
              'file_added_in_rev2.txt'), '');
      assert.equal(
          element._computeCountString(comments, '2', '/COMMIT_MSG', 'comment'),
          '1 comment');
      assert.equal(
          element._computeCommentsStringMobile(comments, '2', '/COMMIT_MSG'),
          '1c');
      assert.equal(
          element._computeDraftsStringMobile(comments, '2', '/COMMIT_MSG'),
          '1d');
      assert.equal(
          element._computeCountString(comments, '2', 'myfile.txt', 'comment'),
          '2 comments');
      assert.equal(
          element._computeCommentsStringMobile(comments, '2', 'myfile.txt'),
          '2c');
      assert.equal(
          element._computeDraftsStringMobile(comments, '2', 'myfile.txt'),
          '2d');
      assert.equal(
          element._computeCountString(comments, '2',
              'file_added_in_rev2.txt', 'comment'), '');
      assert.equal(element._computeCountString(comments, '2',
          'unresolved.file', 'comment'), '3 comments');
      assert.equal(
          element._computeUnresolvedString(comments, [], 2, 'myfile.txt'), '');
      assert.equal(
          element.computeUnresolvedNum(comments, [], 2, 'myfile.txt'), 0);
      assert.equal(
          element._computeUnresolvedString(comments, [], 2, 'unresolved.file'),
          '(1 unresolved)');
      assert.equal(
          element.computeUnresolvedNum(comments, [], 2, 'unresolved.file'), 1);
      assert.equal(
          element._computeUnresolvedString(comments, drafts, 2,
              'unresolved.file'), '');
    });

    test('computed properties', () => {
      assert.equal(element._computeFileStatus('A'), 'A');
      assert.equal(element._computeFileStatus(undefined), 'M');
      assert.equal(element._computeFileStatus(null), 'M');

      assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
      assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
          'clazz invisible');
    });

    test('file review status', () => {
      element._files = [
        {__path: '/COMMIT_MSG'},
        {__path: 'file_added_in_rev2.txt'},
        {__path: 'myfile.txt'},
      ];
      element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
      element._loggedIn = true;
      element.changeNum = '42';
      element.patchRange = {
        basePatchNum: 'PARENT',
        patchNum: '2',
      };
      element.$.fileCursor.setCursorAtIndex(0);

      flushAsynchronousOperations();
      const fileRows =
          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
      const checkSelector = 'input.reviewed[type="checkbox"]';
      const commitMsg = fileRows[0].querySelector(checkSelector);
      const fileAdded = fileRows[1].querySelector(checkSelector);
      const myFile = fileRows[2].querySelector(checkSelector);

      assert.isTrue(commitMsg.checked);
      assert.isFalse(fileAdded.checked);
      assert.isTrue(myFile.checked);

      const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
      const markReviewLabel = commitMsg.nextElementSibling;
      assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
      assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');

      const tapSpy = sandbox.spy(element, '_handleFileListTap');
      MockInteractions.tap(markReviewLabel);
      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
      assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
      assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
      assert.isTrue(tapSpy.lastCall.args[0].defaultPrevented);

      MockInteractions.tap(markReviewLabel);
      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
      assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
      assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
      assert.isTrue(tapSpy.lastCall.args[0].defaultPrevented);
    });

    test('patch set from revisions', () => {
      const expected = [
        {num: 4, desc: 'test'},
        {num: 3, desc: 'test'},
        {num: 2, desc: 'test'},
        {num: 1, desc: 'test'},
      ];
      const patchNums = element.computeAllPatchSets({
        revisions: {
          rev3: {_number: 3, description: 'test'},
          rev1: {_number: 1, description: 'test'},
          rev4: {_number: 4, description: 'test'},
          rev2: {_number: 2, description: 'test'},
        },
      });
      assert.equal(patchNums.length, expected.length);
      for (let i = 0; i < expected.length; i++) {
        assert.deepEqual(patchNums[i], expected[i]);
      }
    });

    test('checkbox shows/hides diff inline', () => {
      element._files = [
        {__path: 'myfile.txt'},
      ];
      element.changeNum = '42';
      element.patchRange = {
        basePatchNum: 'PARENT',
        patchNum: '2',
      };
      element.$.fileCursor.setCursorAtIndex(0);
      sandbox.stub(element, '_expandedPathsChanged');
      flushAsynchronousOperations();
      const fileRows =
          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
      // Because the label surrounds the input, the tap event is triggered
      // there first.
      const showHideLabel = fileRows[0].querySelector('label.show-hide');
      const showHideCheck = fileRows[0].querySelector(
          'input.show-hide[type="checkbox"]');
      assert.isNotOk(showHideCheck.checked);
      MockInteractions.tap(showHideLabel);
      assert.isOk(showHideCheck.checked);
      assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
    });

    test('diff mode correctly toggles the diffs', () => {
      element._files = [
        {__path: 'myfile.txt'},
      ];
      element.changeNum = '42';
      element.patchRange = {
        basePatchNum: 'PARENT',
        patchNum: '2',
      };
      sandbox.spy(element, '_updateDiffPreferences');
      element.$.fileCursor.setCursorAtIndex(0);
      flushAsynchronousOperations();

      // Tap on a file to generate the diff.
      const row = Polymer.dom(element.root)
          .querySelectorAll('.row:not(.header) label.show-hide')[0];

      MockInteractions.tap(row);
      flushAsynchronousOperations();
      const diffDisplay = element.diffs[0];
      element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
      element.set('diffViewMode', 'UNIFIED_DIFF');
      assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
      assert.isTrue(element._updateDiffPreferences.called);
    });

    test('expanded attribute not set on path when not expanded', () => {
      element._files = [
        {__path: '/COMMIT_MSG'},
      ];
      assert.isNotOk(element.$$('.expanded'));
    });

    test('expand_inline_diffs user preference', () => {
      element._files = [
        {__path: '/COMMIT_MSG'},
      ];
      element.changeNum = '42';
      element.patchRange = {
        basePatchNum: 'PARENT',
        patchNum: '2',
      };
      sandbox.stub(element, '_expandedPathsChanged');
      flushAsynchronousOperations();
      const commitMsgFile = Polymer.dom(element.root)
          .querySelectorAll('.row:not(.header) a')[0];

      // Remove href attribute so the app doesn't route to a diff view
      commitMsgFile.removeAttribute('href');
      const togglePathSpy = sandbox.spy(element, '_togglePathExpanded');

      MockInteractions.tap(commitMsgFile);
      flushAsynchronousOperations();
      assert(togglePathSpy.notCalled, 'file is opened as diff view');
      assert.isNotOk(element.$$('.expanded'));

      element._userPrefs = {expand_inline_diffs: true};
      flushAsynchronousOperations();
      MockInteractions.tap(commitMsgFile);
      flushAsynchronousOperations();
      assert(togglePathSpy.calledOnce, 'file is expanded');
      assert.isOk(element.$$('.expanded'));
    });

    test('_togglePathExpanded', () => {
      const path = 'path/to/my/file.txt';
      element.files = [{__path: path}];
      const renderStub = sandbox.stub(element, '_renderInOrder')
          .returns(Promise.resolve());

      assert.equal(element._expandedFilePaths.length, 0);
      element._togglePathExpanded(path);
      flushAsynchronousOperations();

      assert.equal(renderStub.callCount, 1);
      assert.include(element._expandedFilePaths, path);
      element._togglePathExpanded(path);
      flushAsynchronousOperations();

      assert.equal(renderStub.callCount, 2);
      assert.notInclude(element._expandedFilePaths, path);
    });

    test('collapseAllDiffs', () => {
      sandbox.stub(element, '_renderInOrder')
          .returns(Promise.resolve());
      const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
          'handleDiffUpdate');

      const path = 'path/to/my/file.txt';
      element.files = [{__path: path}];
      element._expandedFilePaths = [path];
      element._showInlineDiffs = true;

      element.collapseAllDiffs();
      flushAsynchronousOperations();
      assert.equal(element._expandedFilePaths.length, 0);
      assert.isFalse(element._showInlineDiffs);
      assert.isTrue(cursorUpdateStub.calledOnce);
    });

    test('_expandedPathsChanged', done => {
      sandbox.stub(element, '_reviewFile');
      const path = 'path/to/my/file.txt';
      const diffs = [{
        path,
        reload() {
          done();
        },
      }];
      sinon.stub(element, 'diffs', {
        get() { return diffs; },
      });
      element.push('_expandedFilePaths', path);
    });

    test('filesExpanded value updates to correct enum', () => {
      element._files = [{__path: 'foo.bar'}, {__path: 'baz.bar'}];
      flushAsynchronousOperations();
      assert.equal(element.filesExpanded,
          GrFileListConstants.FilesExpandedState.NONE);
      element.push('_expandedFilePaths', 'baz.bar');
      flushAsynchronousOperations();
      assert.equal(element.filesExpanded,
          GrFileListConstants.FilesExpandedState.SOME);
      element.push('_expandedFilePaths', 'foo.bar');
      flushAsynchronousOperations();
      assert.equal(element.filesExpanded,
          GrFileListConstants.FilesExpandedState.ALL);
      element.collapseAllDiffs();
      flushAsynchronousOperations();
      assert.equal(element.filesExpanded,
          GrFileListConstants.FilesExpandedState.NONE);
      element.expandAllDiffs();
      flushAsynchronousOperations();
      assert.equal(element.filesExpanded,
          GrFileListConstants.FilesExpandedState.ALL);
    });

    suite('_handleFileListTap', () => {
      function testForModifier(modifier) {
        const e = {preventDefault() {}};
        e.detail = {sourceEvent: {}};
        e.target = {
          dataset: {path: '/test'},
          classList: element.classList,
        };

        e.detail.sourceEvent[modifier] = true;

        const togglePathStub = sandbox.stub(element, '_togglePathExpanded');
        element._userPrefs = {expand_inline_diffs: true};

        element._handleFileListTap(e);
        assert.isFalse(togglePathStub.called);

        e.detail.sourceEvent[modifier] = false;
        element._handleFileListTap(e);
        assert.equal(togglePathStub.callCount, 1);

        element._userPrefs = {expand_inline_diffs: false};
        element._handleFileListTap(e);
        assert.equal(togglePathStub.callCount, 1);
      }

      test('_handleFileListTap meta', () => {
        testForModifier('metaKey');
      });

      test('_handleFileListTap ctrl', () => {
        testForModifier('ctrlKey');
      });
    });

    test('_renderInOrder', done => {
      const reviewStub = sandbox.stub(element, '_reviewFile');
      let callCount = 0;
      const diffs = [{
        path: 'p0',
        reload() {
          assert.equal(callCount++, 2);
          return Promise.resolve();
        },
      }, {
        path: 'p1',
        reload() {
          assert.equal(callCount++, 1);
          return Promise.resolve();
        },
      }, {
        path: 'p2',
        reload() {
          assert.equal(callCount++, 0);
          return Promise.resolve();
        },
      }];
      element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
          .then(() => {
            assert.isFalse(reviewStub.called);
            assert.isTrue(loadCommentSpy.called);
            done();
          });
    });

    test('_renderInOrder logged in', done => {
      element._isLoggedIn = true;
      const reviewStub = sandbox.stub(element, '_reviewFile');
      let callCount = 0;
      const diffs = [{
        path: 'p0',
        reload() {
          assert.equal(reviewStub.callCount, 2);
          assert.equal(callCount++, 2);
          return Promise.resolve();
        },
      }, {
        path: 'p1',
        reload() {
          assert.equal(reviewStub.callCount, 1);
          assert.equal(callCount++, 1);
          return Promise.resolve();
        },
      }, {
        path: 'p2',
        reload() {
          assert.equal(reviewStub.callCount, 0);
          assert.equal(callCount++, 0);
          return Promise.resolve();
        },
      }];
      element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
          .then(() => {
            assert.equal(reviewStub.callCount, 3);
            done();
          });
    });

    test('_loadingChanged fired from reload in debouncer', done => {
      element.changeNum = 123;
      element.patchRange = {patchNum: 12};
      element._files = [{__path: 'foo.bar'}];

      element.reload().then(() => {
        assert.isFalse(element._loading);
        element.flushDebouncer('loading-change');
        assert.isFalse(element.classList.contains('loading'));
        done();
      });
      assert.isTrue(element._loading);
      assert.isFalse(element.classList.contains('loading'));
      element.flushDebouncer('loading-change');
      assert.isTrue(element.classList.contains('loading'));
    });

    test('_loadingChanged does not set class when there are no files', () => {
      element.changeNum = 123;
      element.patchRange = {patchNum: 12};
      element.reload();
      assert.isTrue(element._loading);
      element.flushDebouncer('loading-change');
      assert.isFalse(element.classList.contains('loading'));
    });

    test('no execute _computeDiffURL before patchNum is knwon', done => {
      const urlStub = sandbox.stub(element, '_computeDiffURL');
      element.change = {_number: 123};
      element.patchRange = {patchNum: undefined, basePatchNum: 'PARENT'};
      element._files = [{__path: 'foo/bar.cpp'}];
      flush(() => {
        assert.isFalse(urlStub.called);
        element.set('patchRange.patchNum', 4);
        flush(() => {
          assert.isTrue(urlStub.called);
          done();
        });
      });
    });
  });

  suite('gr-file-list inline diff tests', () => {
    let element;
    let sandbox;

    const setupDiff = function(diff) {
      const mock = document.createElement('mock-diff-response');
      diff._diff = mock.diffResponse;
      diff.comments = {
        left: [],
        right: [],
      };
      diff.prefs = {
        context: 10,
        tab_size: 8,
        font_size: 12,
        line_length: 100,
        cursor_blink_rate: 0,
        line_wrapping: false,
        intraline_difference: true,
        show_line_endings: true,
        show_tabs: true,
        show_whitespace_errors: true,
        syntax_highlighting: true,
        auto_hide_diff_table_header: true,
        theme: 'DEFAULT',
        ignore_whitespace: 'IGNORE_NONE',
      };
      diff._renderDiffTable();
    };

    const renderAndGetNewDiffs = function(index) {
      const diffs =
          Polymer.dom(element.root).querySelectorAll('gr-diff');

      for (let i = index; i < diffs.length; i++) {
        setupDiff(diffs[i]);
      }

      element._updateDiffCursor();
      element.$.diffCursor.handleDiffUpdate();
      return diffs;
    };

    setup(done => {
      sandbox = sinon.sandbox.create();
      stub('gr-rest-api-interface', {
        getLoggedIn() { return Promise.resolve(true); },
        getPreferences() { return Promise.resolve({}); },
        getDiffComments() { return Promise.resolve({}); },
        getDiffRobotComments() { return Promise.resolve({}); },
        getDiffDrafts() { return Promise.resolve({}); },
      });
      stub('gr-date-formatter', {
        _loadTimeFormat() { return Promise.resolve(''); },
      });
      stub('gr-diff', {
        reload() { return Promise.resolve(); },
      });

      // Element must be wrapped in an element with direct access to the
      // comment API.
      commentApiWrapper = fixture('basic');
      element = commentApiWrapper.$.fileList;
      loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');

      // Stub methods on the changeComments object after changeComments has
      // been initalized.
      commentApiWrapper.loadComments().then(() => {
        sandbox.stub(element.changeComments, 'getPaths').returns({});
        sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
            .returns({meta: {}, left: [], right: []});
        done();
      });
      element.numFilesShown = 75;
      element.selectedIndex = 0;
      element._files = [
        {__path: '/COMMIT_MSG', lines_inserted: 9},
        {
          __path: 'file_added_in_rev2.txt',
          lines_inserted: 1,
          lines_deleted: 1,
          size_delta: 10,
          size: 100,
        },
        {
          __path: 'myfile.txt',
          lines_inserted: 1,
          lines_deleted: 1,
          size_delta: 10,
          size: 100,
        },
      ];
      element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
      element._loggedIn = true;
      element.changeNum = '42';
      element.patchRange = {
        basePatchNum: 'PARENT',
        patchNum: '2',
      };
      sandbox.stub(window, 'fetch', () => {
        return Promise.resolve();
      });
      flushAsynchronousOperations();
    });

    teardown(() => {
      sandbox.restore();
    });

    test('cursor with individually opened files', () => {
      MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
      flushAsynchronousOperations();
      let diffs = renderAndGetNewDiffs(0);
      const diffStops = diffs[0].getCursorStops();

      // 1 diff should be rendered.
      assert.equal(diffs.length, 1);

      // No line number is selected.
      assert.isFalse(diffStops[10].classList.contains('target-row'));

      // Tapping content on a line selects the line number.
      MockInteractions.tap(Polymer.dom(
          diffStops[10]).querySelectorAll('.contentText')[0]);
      flushAsynchronousOperations();
      assert.isTrue(diffStops[10].classList.contains('target-row'));

      // Keyboard shortcuts are still moving the file cursor, not the diff
      // cursor.
      MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
      flushAsynchronousOperations();
      assert.isTrue(diffStops[10].classList.contains('target-row'));
      assert.isFalse(diffStops[11].classList.contains('target-row'));

      // The file cusor is now at 1.
      assert.equal(element.$.fileCursor.index, 1);
      MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
      flushAsynchronousOperations();

      diffs = renderAndGetNewDiffs(1);
      // Two diffs should be rendered.
      assert.equal(diffs.length, 2);
      const diffStopsFirst = diffs[0].getCursorStops();
      const diffStopsSecond = diffs[1].getCursorStops();

      // The line on the first diff is stil selected
      assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
      assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
    });

    test('cursor with toggle all files', () => {
      MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
      flushAsynchronousOperations();

      const diffs = renderAndGetNewDiffs(0);
      const diffStops = diffs[0].getCursorStops();

      // 1 diff should be rendered.
      assert.equal(diffs.length, 3);

      // No line number is selected.
      assert.isFalse(diffStops[10].classList.contains('target-row'));

      // Tapping content on a line selects the line number.
      MockInteractions.tap(Polymer.dom(
          diffStops[10]).querySelectorAll('.contentText')[0]);
      flushAsynchronousOperations();
      assert.isTrue(diffStops[10].classList.contains('target-row'));

      // Keyboard shortcuts are still moving the file cursor, not the diff
      // cursor.
      MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
      flushAsynchronousOperations();
      assert.isFalse(diffStops[10].classList.contains('target-row'));
      assert.isTrue(diffStops[11].classList.contains('target-row'));

      // The file cusor is still at 0.
      assert.equal(element.$.fileCursor.index, 0);
    });

    suite('n key presses', () => {
      let nKeySpy;
      let nextCommentStub;
      let nextChunkStub;
      let fileRows;
      setup(() => {
        sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
        nKeySpy = sandbox.spy(element, '_handleNKey');
        nextCommentStub = sandbox.stub(element.$.diffCursor,
            'moveToNextCommentThread');
        nextChunkStub = sandbox.stub(element.$.diffCursor,
            'moveToNextChunk');
        fileRows =
            Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
      });
      test('n key with all files expanded and no shift key', () => {
        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
        flushAsynchronousOperations();

        // Handle N key should return before calling diff cursor functions.
        MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
        assert.isTrue(nKeySpy.called);
        assert.isFalse(nextCommentStub.called);

        // This is also called in diffCursor.moveToFirstChunk.
        assert.equal(nextChunkStub.callCount, 1);
        assert.isFalse(!!element._showInlineDiffs);
      });

      test('n key with all files expanded and shift key', () => {
        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
        flushAsynchronousOperations();

        MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
        assert.isTrue(nKeySpy.called);
        assert.isFalse(nextCommentStub.called);

        // This is also called in diffCursor.moveToFirstChunk.
        assert.equal(nextChunkStub.callCount, 1);
        assert.isFalse(!!element._showInlineDiffs);
      });

      test('n key without all files expanded and shift key', () => {
        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
        flushAsynchronousOperations();

        MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
        assert.isTrue(nKeySpy.called);
        assert.isFalse(nextCommentStub.called);

        // This is also called in diffCursor.moveToFirstChunk.
        assert.equal(nextChunkStub.callCount, 2);
        assert.isTrue(element._showInlineDiffs);
      });

      test('n key without all files expanded and no shift key', () => {
        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
        flushAsynchronousOperations();

        MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
        assert.isTrue(nKeySpy.called);
        assert.isTrue(nextCommentStub.called);

        // This is also called in diffCursor.moveToFirstChunk.
        assert.equal(nextChunkStub.callCount, 1);
        assert.isTrue(element._showInlineDiffs);
      });
    });

    test('_openSelectedFile behavior', () => {
      const _files = element._files;
      element.set('_files', []);
      const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
      // Noop when there are no files.
      element._openSelectedFile();
      assert.isFalse(navStub.called);

      element.set('_files', _files);
      flushAsynchronousOperations();
       // Navigates when a file is selected.
      element._openSelectedFile();
      assert.isTrue(navStub.called);
    });

    test('_displayLine', () => {
      sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false);
      sandbox.stub(element, 'modifierPressed', () => false);
      element._showInlineDiffs = true;
      const mockEvent = {preventDefault() {}};

      element._displayLine = false;
      element._handleDownKey(mockEvent);
      assert.isTrue(element._displayLine);

      element._displayLine = false;
      element._handleUpKey(mockEvent);
      assert.isTrue(element._displayLine);

      element._displayLine = true;
      element._handleEscKey(mockEvent);
      assert.isFalse(element._displayLine);
    });

    suite('editLoaded behavior', () => {
      const isVisible = el => {
        assert.ok(el);
        return getComputedStyle(el).getPropertyValue('display') !== 'none';
      };

      test('reviewed checkbox', () => {
        const alertStub = sandbox.stub();
        const saveReviewStub = sandbox.stub(element, '_saveReviewedState');

        element.addEventListener('show-alert', alertStub);
        element.editLoaded = false;
        // Reviewed checkbox should be shown.
        assert.isTrue(isVisible(element.$$('.reviewed')));
        MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
        assert.isFalse(alertStub.called);
        assert.isTrue(saveReviewStub.calledOnce);

        element.editLoaded = true;
        flushAsynchronousOperations();

        assert.isFalse(isVisible(element.$$('.reviewed')));
        MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
        assert.isTrue(alertStub.called);
        assert.isTrue(saveReviewStub.calledOnce);
      });

      test('_getReviewedFiles does not call API', () => {
        const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles');
        element.editLoaded = true;
        return element._getReviewedFiles().then(files => {
          assert.equal(files.length, 0);
          assert.isFalse(apiSpy.called);
        });
      });
    });

    test('editing actions', () => {
      element.editLoaded = true;
      const editControls =
          Polymer.dom(element.root).querySelectorAll('.row:not(.header)')
            .map(row => row.querySelector('gr-edit-file-controls'));

      // Commit message should not have edit controls.
      assert.isTrue(editControls[0].classList.contains('invisible'));
    });
  });
  a11ySuite('basic');
</script>
